Skip to content

fix(security): gate control-plane writes, hard-deny lifecycle files - #617

Merged
Aaronontheweb merged 4 commits into
devfrom
fix/control-plane-write-approval
Apr 12, 2026
Merged

fix(security): gate control-plane writes, hard-deny lifecycle files#617
Aaronontheweb merged 4 commits into
devfrom
fix/control-plane-write-approval

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Apr 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Prevents agents from silently editing ~/.netclaw/config/netclaw.json and other control-plane files, which can trigger daemon restart and drop the active session mid-turn.
  • Splits ToolPathPolicy into three independent deny surfaces: write-deny (Tier 1 hard deny for secrets, keys, SQLite DB, pid/lock/restart-manifest), read-deny (credentials only), and shell-indicators (narrow enough to keep diagnostics like ls ~/.netclaw/config working).
  • Adds FilePathApprovalMatcher for argument-aware control-plane write/edit approvals, with path-scoped patterns like file_write:control-plane:netclaw.json.
  • Ensures approval-mode precedence is deterministic for path-aware calls: matcher-specific key -> base tool key -> personal fail-closed -> audience default.
  • Fixes approve-once retry behavior so bypass matching uses the same filtered unapproved pattern set that the user saw in the prompt.
  • Extends shell resource deny coverage for lifecycle files (netclaw.db, netclaw.pid, netclaw.lock, cache/restart-manifest.json) while keeping operation hard-deny precedence.

Behavior changes

Before After
Control-plane write with partial approval policy could miss intended gating Control-plane write/edit consistently resolve approval mode with matcher+base fallback precedence
Approve Once could reprompt on immediate retry in mixed approved/unapproved pattern cases Immediate retry uses filtered unapproved patterns and succeeds without a second prompt
Tier 1 lifecycle files were hard-denied for file tools but not fully represented in shell indicator list Shell path-policy checks also cover lifecycle/control-plane files
shell_execute: ls ~/.netclaw/config risked collateral blocking with broad indicators Diagnostics remain allowed; sensitive file references are denied

Files

  • src/Netclaw.Security/ToolPathPolicy.cs - deny surface refactor and shell indicator behavior
  • src/Netclaw.Security/IToolApprovalMatcher.cs - matcher extension points used for path-aware mode keys/fail-closed
  • src/Netclaw.Actors/Tools/FilePathApprovalMatcher.cs - control-plane path-aware matcher
  • src/Netclaw.Actors/Tools/ToolAccessPolicy.cs - approval mode resolution precedence and matcher wiring
  • src/Netclaw.Actors/Tools/DispatchingToolExecutor.cs - filtered unapproved-set approve-once matching
  • src/Netclaw.Actors/Tools/File{Write,Edit,Read}Tool.cs + FileToolErrors.cs - control-plane/credential deny behavior and copy
  • src/Netclaw.Daemon/Program.cs - deny-list and matcher composition wiring
  • docs/runbooks/tool-approval-gates.md - layered policy model + path-aware approval docs
  • src/Netclaw.Configuration/Schemas/netclaw-config.v1.schema.json - matcher-specific override key docs

OpenSpec

  • Synced to main spec: openspec/specs/tool-approval-gates/spec.md
  • Archived completed change: openspec/changes/archive/2026-04-12-tool-approval-composition-fixes/

Test plan

  • dotnet test src/Netclaw.Actors.Tests/ --filter "ToolApprovalGateTests|DispatchingToolExecutorTests|SessionToolExecutionPipelineTests|ShellToolTests" (51/51)
  • dotnet test src/Netclaw.Security.Tests/ --filter "ToolPathPolicyTests|ShellCommandPolicyTests" (60/60)
  • dotnet test Netclaw.slnx (full suite green)
  • dotnet slopwatch analyze (0 issues)
  • Manual repro: Personal session invokes file_write against ~/.netclaw/config/netclaw.json -> approval prompt appears with control-plane pattern; declined -> session survives; approved -> write proceeds
  • Manual regression: shell_execute: ls ~/.netclaw/config still succeeds
  • Manual Tier 1: file_write against ~/.netclaw/netclaw.db -> immediate fail-loud error, no approval prompt

An agent session edited ~/.netclaw/config/netclaw.json via file_write; the
config watcher detected the change and triggered a full daemon restart,
dropping the session mid-turn with no warning or attribution. ToolPathPolicy
only protected secrets.json, webhooks/, and keys/ — the rest of the control
plane was writable by any tool with the `file` grant, and Personal audience
has no root-containment, so the deny list was the entire security boundary.

Split ToolPathPolicy into three independent enforcement surfaces: write
deny, read deny, and a narrow shell indicator list (so directory-scoped
write-deny entries don't bleed into the substring scan that gates
`shell_execute` — otherwise `ls ~/.netclaw/config` would regress).

Tier 1 (hard deny, no approval possible): secrets, keys, netclaw.db,
pid/lock files, restart-manifest.json. Corrupting these is unrecoverable
and no legitimate agent flow edits them.

Tier 2 (approval-gated via FilePathApprovalMatcher): everything under
~/.netclaw/config/ that isn't Tier 1 — netclaw.json, devices.json,
tool-approvals.json, mcp-oauth-metadata.json, webhooks/*.json. The matcher
inspects the target path and routes control-plane writes to a
`file_write:control-plane` approval-mode key with per-path patterns so
approving netclaw.json doesn't implicitly approve tool-approvals.json.
Fail-closed default in GetMissingApprovalPolicyDefaultMode: when
ApprovalPolicy is null on Personal audience, control-plane writes still
require interactive approval, matching the existing fail-closed behavior
for shell_execute.

FileReadTool switches from IsDenied to the narrower IsReadDenied so the
agent can still read netclaw.json for diagnostics while writes are gated.
Error messages on all three file tools now name the offending path and
point at the right escape hatch (`netclaw doctor --fix`, `netclaw secrets
set`) instead of a generic "protected by security policy" string.
…ogic

Review followups on the control-plane write fix:

- Extract duplicated control-plane deny message from FileWriteTool and
  FileEditTool into FileToolErrors so the copy can't drift.
- Move the "does this invocation require fail-closed approval?" decision
  onto IToolApprovalMatcher (new IsFailClosedOnPersonal method) instead of
  inspecting approval-mode-key string suffixes from ToolAccessPolicy. The
  shell fail-closed default now lives in ShellApprovalMatcher where it
  belongs; FilePathApprovalMatcher answers the same question for
  control-plane paths without ToolAccessPolicy needing to know its key
  format.
- Drop unused ControlPlanePatternPrefix const and narrative/incident
  comments that referenced the originating fix rather than the invariant.
Ensure matcher and base-key precedence plus approve-once retries use the same filtered pattern set so approval prompts stay deterministic. Harden shell resource deny coverage and sync/archive the related OpenSpec updates.
@Aaronontheweb Aaronontheweb added .NET Pull requests that update .NET code documentation Improvements or additions to documentation security Security-related changes sessions LLM session actor, turn lifecycle, pipelines labels Apr 12, 2026
@Aaronontheweb
Aaronontheweb marked this pull request as ready for review April 12, 2026 23:32
@Aaronontheweb
Aaronontheweb enabled auto-merge (squash) April 12, 2026 23:32
@Aaronontheweb
Aaronontheweb merged commit ca389b5 into dev Apr 12, 2026
4 checks passed
@Aaronontheweb
Aaronontheweb deleted the fix/control-plane-write-approval branch April 12, 2026 23:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation .NET Pull requests that update .NET code security Security-related changes sessions LLM session actor, turn lifecycle, pipelines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant